package com.virjar.xposedhooktool.tool.newsocket;

import android.support.annotation.NonNull;

import com.virjar.xposedhooktool.tool.log.NewLogUtil;

import org.apache.commons.io.IOUtils;
import org.apache.commons.io.output.ByteArrayOutputStream;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Socket;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.zip.GZIPInputStream;

/**
 * Created by virjar on 2018/4/26.<br>在读取数据的时候，产生一个拷贝
 */
public class InputStreamWrapper extends InputStream {
    private Socket socket;
    private InputStream delegate;
    private ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
    private static final String tag = "SocketMonitor";
    private String track = null;

    InputStreamWrapper(InputStream delegate, Socket socket) {
        this.delegate = delegate;
        this.socket = socket;
    }

    @Override
    public int read() throws IOException {
        int data = delegate.read();
        if (data > 0) {
            byteArrayOutputStream.write(data);
        } else if (data < 0) {
            streamEnd = true;
            checkAndOut();
        }
        return data;
    }

    @Override
    public int read(@NonNull byte[] b) throws IOException {
        int readSize = delegate.read(b);
        if (readSize > 0) {
            byteArrayOutputStream.write(b, 0, readSize);
        } else if (readSize < 0) {
            streamEnd = true;
            checkAndOut();
        }
        return readSize;
    }

    @Override
    public int read(@NonNull byte[] b, int off, int len) throws IOException {
        int readSize = delegate.read(b, off, len);
        if (readSize > 0) {
            byteArrayOutputStream.write(b, off, readSize);
        } else if (readSize < 0) {
            streamEnd = true;
            checkAndOut();
        }
        return readSize;
    }

    @Override
    public int available() throws IOException {
        return delegate.available();
    }

    @Override
    public void close() throws IOException {
        delegate.close();
        streamEnd = true;
        checkAndOut();
    }

    private byte[] headData = null;

    private static final int printStateInit = 0;
    private static final int printStateIsHttpResponse = 1;
    private static final int printStateReadHttpHeader = 2;
    private static final int printStateLogPrinted = 3;
    private static final int printStateNotHttpResponse = 4;
    private boolean streamEnd = false;
    private static final int printStateReadBody = 6;
    private static final int printStateReadChuncked = 7;

    private int printState = printStateInit;

    private long headerSplitByte = 0;
    private int contentLength = 0;

    private boolean isGzip = false;

    private synchronized void checkAndOut() {
        if (printState == printStateLogPrinted) {
            return;
        }
        if (!NewLogUtil.isComponentStarted()) {
            return;
        }

        if (printState == printStateNotHttpResponse && streamEnd) {
            printState = printStateLogPrinted;
            logStream(byteArrayOutputStream.toInputStream());
            return;
        }

        if (byteArrayOutputStream.size() < 10) {
            //当前读到的数据太短，无法输出
            return;
        }
        if (printState == printStateNotHttpResponse) {
            return;
        }
        if (printState == printStateInit) {
            //test if the input data may be a http response
            byte[] buf = new byte[128];
            try {
                //这个inputStream是一个view，不会消耗额外空间资源，所以我们使用了就丢弃
                InputStream inputStream = byteArrayOutputStream.toInputStream();
                int read = inputStream.read(buf, 0, 128);
                //判断是否是http协议，通过头部的HTTP/前缀
                if (maybeHttpResponse(buf, read)) {
                    printState = printStateIsHttpResponse;
                } else {
                    printState = printStateNotHttpResponse;
                }
            } catch (IOException e) {
                e.printStackTrace();
                printState = printStateNotHttpResponse;
            }
        }

        //then the data may be http protocol,
        if (printState == printStateIsHttpResponse) {
            //寻找http的头部
            try {
                int rlen = 0;
                InputStream inputStream = byteArrayOutputStream.toInputStream();
                byte[] buf = new byte[HttpStreamUtil.BUFSIZE];
                int read = inputStream.read(buf, 0, HttpStreamUtil.BUFSIZE);
                while (read > 0) {
                    rlen += read;
                    headerSplitByte = HttpStreamUtil.findHeaderEnd(buf, rlen);
                    if (headerSplitByte > 0) {
                        break;
                    }
                    read = inputStream.read(buf, rlen, HttpStreamUtil.BUFSIZE - rlen);
                }
                if (headerSplitByte == 0) {
                    //还没有读取到完整的header数据
                    return;
                }
                // now parse http header
                if (null == this.headers) {
                    this.headers = new HashMap<>();
                } else {
                    this.headers.clear();
                }

                // Create a BufferedReader for parsing the header.
                BufferedReader hin = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(buf, 0, rlen)));

                // Decode the header into parms and header java properties
                if (!decodeHeader(hin, this.headers)) {
                    printState = printStateNotHttpResponse;
                    return;
                }
                printState = printStateReadHttpHeader;

            } catch (IOException e) {
                e.printStackTrace();
                printState = printStateNotHttpResponse;
            }
        }
        //头部已经解析成功，解析数据长度
        if (printState == printStateReadHttpHeader) {
            String contentLengthStr = headers.get("Content-Length".toLowerCase(Locale.US));
            if (StringUtils.isNotBlank(contentLengthStr)) {
                contentLength = NumberUtils.toInt(StringUtils.trim(contentLengthStr));
                printState = printStateReadBody;
            } else {
                //test if body is chuncked transfer
                if (StringUtils.equalsIgnoreCase(headers.get("Transfer-Encoding".toLowerCase(Locale.US)), "chunked")) {
                    printState = printStateReadChuncked;
                } else {
                    //warning http response ,lost content length
                    printState = printStateNotHttpResponse;
                }
            }
            //test for gzip
            //Content-Type: application/zip
            if (StringUtils.equalsIgnoreCase(headers.get("Content-Encoding".toLowerCase(Locale.US)), "gzip")) {
                isGzip = true;
            }
            if (StringUtils.containsIgnoreCase(headers.get("Content-Type".toLowerCase(Locale.US)), "application/zip")) {
                isGzip = true;
            }

            //解析头部数据
            try {
                ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
                headData = IOUtils.toByteArray(byteArrayInputStream, headerSplitByte);
            } catch (IOException e) {
                // not happened
                e.printStackTrace();
                printState = printStateNotHttpResponse;
            }
        }


        //报文完整
        if (printState == printStateReadBody && byteArrayOutputStream.size() >= contentLength + headerSplitByte) {
            ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
            byteArrayInputStream.skip(headerSplitByte);
            InputStream httpContent = byteArrayInputStream;
            if (isGzip) {
                try {
                    httpContent = new GZIPInputStream(httpContent);
                } catch (IOException e) {
                    isGzip = false;
                }
            }
            if (StringUtils.startsWithIgnoreCase(headers.get("Content-Type".toLowerCase(Locale.US)), "image/")) {
                httpContent = new ByteArrayInputStream("this content is a image!".getBytes());
            }
            final InputStream finalInputStream = httpContent;
            track = NewLogUtil.getTrack();
            NewLogUtil.outLog(new NewLogUtil.LogMessage() {
                @Override
                public void handle(OutputStream outputStream) throws IOException {
                    outputStream.write(getPrefix().getBytes());
                    outputStream.write(headData);
                    IOUtils.copy(finalInputStream, outputStream);
                }
            });
            //the  end state
            printState = printStateLogPrinted;
            return;
        }

        if (printState == printStateReadChuncked) {
            //数据分片处理
            //TODO

        }

        if (printState == printStateNotHttpResponse && streamEnd) {
            printState = printStateLogPrinted;
            logStream(byteArrayOutputStream.toInputStream());
        }
    }


    private Map<String, String> headers;

    private static final String httpResponseMagic = "HTTP/";

    private boolean maybeHttpResponse(byte[] data, int dataLength) {
        //first,find the start of data
        int pos = 0;
        while (pos < dataLength) {
            if (!isWhitespace(data[pos])) {
                break;
            }
        }
        if (pos + httpResponseMagic.length() >= dataLength) {
            return false;
        }
        //then the HTTP/1.1 200 OK
        //read 5 byte
        return StringUtils.equalsIgnoreCase(httpResponseMagic, new String(data, pos, httpResponseMagic.length()));
    }

    private static boolean isWhitespace(final byte ch) {
        return ch == HttpStreamUtil.SP || ch == HttpStreamUtil.HT || ch == HttpStreamUtil.CR || ch == HttpStreamUtil.LF;
    }

    private void decodeData(ByteArrayInputStream inputStream) {
        try {
            // Read the first 8192 bytes.
            // The full header should fit in here.
            // Apache's default header limit is 8KB.
            // Do NOT assume that a single read will get the entire header
            // at once!
            byte[] buf = new byte[HttpStreamUtil.BUFSIZE];
            int splitbyte = 0;
            int rlen = 0;

            inputStream.mark(HttpStreamUtil.BUFSIZE);
            int read = inputStream.read(buf, 0, HttpStreamUtil.BUFSIZE);
            if (read == -1) {
                // socket was been closed,not happend
                return;
            }

            //判断是否是http协议，通过头部的HTTP/前缀
            if (!maybeHttpResponse(buf, read)) {
                // not http protocol
                inputStream.reset();
                logStream(inputStream);
                return;
            }
            while (read > 0) {
                rlen += read;
                splitbyte = HttpStreamUtil.findHeaderEnd(buf, rlen);
                if (splitbyte > 0) {
                    break;
                }
                read = inputStream.read(buf, rlen, HttpStreamUtil.BUFSIZE - rlen);
            }

            if (splitbyte < rlen) {
                inputStream.reset();
                inputStream.skip(splitbyte);
            }

            if (null == this.headers) {
                this.headers = new HashMap<>();
            } else {
                this.headers.clear();
            }

            // Create a BufferedReader for parsing the header.
            BufferedReader hin = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(buf, 0, rlen)));

            // Decode the header into parms and header java properties
            if (!decodeHeader(hin, this.headers)) {
                inputStream.reset();
                logStream(inputStream);
                return;
            }

            InputStream remainData = inputStream;
            //now print the http request body,hand gzip
            //test gzip
            String contentEncoding = headers.get("Content-Encoding".toLowerCase(Locale.US));

            //Content-Type: application/zip
            String contentType = headers.get("Content-Type".toLowerCase(Locale.US));

            if (StringUtils.equalsIgnoreCase(headers.get("Transfer-Encoding".toLowerCase(Locale.US)), "chunked")) {
                remainData = new ChunkedInputStream(remainData);
            }
            boolean isGzip = false;
            if (StringUtils.equalsIgnoreCase(contentEncoding, "gzip")) {
                isGzip = true;
            }

            if (StringUtils.containsIgnoreCase(contentType, "application/zip")) {
                isGzip = true;
            }

            inputStream.reset();
            final byte[] headData = IOUtils.toByteArray(inputStream, splitbyte);

            if (StringUtils.startsWithIgnoreCase(contentType, "image/")) {
                remainData = new ByteArrayInputStream("this content is a image!".getBytes());
            } else if (isGzip) {
                remainData = new GZIPInputStream(remainData);
            }

            final InputStream finalInputStream = remainData;
            NewLogUtil.outLog(new NewLogUtil.LogMessage() {
                @Override
                public void handle(OutputStream outputStream) throws IOException {
                    outputStream.write(getPrefix().getBytes());
                    outputStream.write(headData);
                    IOUtils.copy(finalInputStream, outputStream);
                }
            });

        } catch (IOException e) {
            inputStream.reset();
            logStream(inputStream);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    private boolean decodeHeader(BufferedReader in, Map<String, String> headers) {
        try {
            // Read the request line
            String inLine = in.readLine();
            if (inLine == null) {
                // not happen
                return false;
            }

            String line = in.readLine();
            while (line != null && !line.trim().isEmpty()) {
                int p = line.indexOf(':');
                if (p >= 0) {
                    headers.put(line.substring(0, p).trim().toLowerCase(Locale.US), line.substring(p + 1).trim());
                }
                line = in.readLine();
            }
            return true;
        } catch (IOException ioe) {
            //the exception will not happen
            ioe.printStackTrace();
            return false;
        }
    }

    private void logStream(InputStream inputStream) {
        if (!NewLogUtil.isComponentStarted()) {
            return;
        }
        NewLogUtil.outLog(getPrefix(), inputStream);
    }

    private String getPrefix() {
        int localPort = socket.getLocalPort();
        int remotePort = socket.getPort();
        String remoteAddress = socket.getInetAddress().getHostAddress();
        String sessionID = "socket_" + socket.hashCode();
        if (StringUtils.isBlank(track)) {
            track = NewLogUtil.getTrack();
        }
        return "Socket response, local port: " + localPort +
                " remote address:" +
                remoteAddress +
                ":" +
                remotePort +
                "\n" +
                "sessionID:" +
                sessionID +
                "\n" +
                "StackTrace:" +
                track +
                "data:" +
                "\n";
    }

    @Override
    public synchronized void mark(int readlimit) {
        delegate.mark(readlimit);
    }

    @Override
    public synchronized void reset() throws IOException {
        delegate.reset();
    }

    @Override
    public boolean markSupported() {
        return delegate.markSupported();
    }
}
